简单的测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class TestObject
{
static int ObjCount = 0;
static int ObjId = 0;
private Timer tm;
private int id = 0;
private int sec = 0;

public TestObject()
{
id = ObjId++;
ObjCount++;
}

~TestObject()
{
ObjCount--;
Console.WriteLine("~TestObject:{0}", id);
tm.Stop();
}

void onTimer(object sender, ElapsedEventArgs eventArgs)
{
Console.WriteLine("** timer id:{1}, objcount:{0}", ObjCount, id);
if (++sec >= 3)
{
tm.Stop();
tm.Elapsed -= onTimer;
Console.WriteLine("** stop: {0}", mid);
}
}

public void Start()
{
tm = new Timer(1000);
var mid = id;

// 变化3
// tm.Elapsed += onTimer;

tm.Elapsed += delegate(object sender, ElapsedEventArgs eventArgs)
{
Console.WriteLine("** timer id:{1}, objcount:{0}", ObjCount, this.id);
// 变化1
// Console.WriteLine("** timer id:{1}, objcount:{0}", ObjCount, mid);

// 变化2
// if (sec++ > 5) tm.Stop();
};
tm.Start();
}

public void Stop()
{
Console.WriteLine("stop:{0}",id);
tm.Stop();
}
}

变化和对比:

  • 在timer中引用this与否(使用this.id还是临时变量mid)
  • timer是否结束
  • timer结束后是否显式删除订阅(用即时定义delegate的方式显然就不能删除了,因此只能用成员函数的方式)

实验一

timer不结束,且引用了成员变量this.id
.netcore和mono都一样,无论创建多少个对象,gc时一个都回收不了。
原因:timer在运行,是活跃对象,且timer引用了this,导致this不能gc。

实验二

timer不结束,将this.id改为临时变量mid
.netcore和mono都一样,所有对象都能gc
原因:timer虽然都是活跃的,但不再引用this,this被gc,在析构中停止timer,一起over。

实验三

timer过一会自己停止,还是引用this.id
这时就出现差别了:
.netcore:在timer停止后,所有对象都能立即被gc(立即意指调用一次GC.Collect的结果)
mono:在timer停止后,大部分对象能被gc,但某些对象能活很久,只有不停创建新对象的过程中,那些老对象才看似随机的被gc掉,甚至出现3号对象一直不gc,4/5/6都gc了,创建7号时才看到3号gc的现象。而且很大概率总有1个对象长久不能gc,剩余对象数为0的情形很难出现一次。

实验四

在三的基础上,将匿名函数改为成员函数,在timer停止后再删除订阅
.netcore:与之前一样,都能gc。且无论删除订阅与否,都很快gc完。
mono:也全都能gc了😂。但是如果不删除订阅,则表现与三一样,有的对象活很久很难全部彻底gc。

结论

  • mono gc看起来更保守,.netcore则更加“激进彻底”
  • 从三、四对比来看,事件订阅是否删除,很大程度影响了mono gc的判定,理论上即使不删除,owner与timer都已经是不可达对象,但为什么删除后gc就很快?猜测可能是timer的内部实现,也许还有另一个可达列表记录(缓存)着所有timer,视其激活与否来延迟摘掉它,所以表现为最终状态都会gc,但有时候延迟很久,而删除定阅则从根本上解除了owner的所有引用,不再被timer这条灰线牵绊。

起因

之所以颇费心思测试对比这个,是因为在写xamarin程序时的一点遭遇:

  1. 发现有的Page在关闭后还会响应网络事件
  2. 当然最好的做法是为Page实现OnDisappearing函数,在这里注销事件
  3. 但是想着Page在关闭后应该没有引用了,会被gc, 那只要在添加事件订阅时使用WeakReference技巧,就可以在检测到gc时自动删除订阅,比2中的方法省事一点
  4. 结果却发现Page一直没有gc,最后不断删除无关代码排除大法,定位到是一个timer的使用所致。虽然当时就想到可能与订阅有关,并且改成成员函数式的订阅加删除后,确实就会gc了,但还是想剥离关键代码做一个专门的测试
  5. 初次测试由于没考虑到.netcore和mono的差别,只在.netcore环境下跑,结果发现完全没有问题,导致对本已解决的xamarin bug又自我怀疑,做了大量重复排查的无用功
  6. 终于想到xamarin是基于mono,而通常新建的console app都是基于.netcore,两者可能在内部实现上并不一样,于是再分别测试各种情况,最终确认mono版的表现与xamarin上是一致的

参考:
https://docs.microsoft.com/en-us/dotnet/standard/net-standard